import Lang from "../Lang";
import Tokenizer from "./Tokenizer";
export default class Parser {
    private tn: Tokenizer;
    constructor(proto: string) {
        this.tn = new Tokenizer(proto);
    }
    public parse(): {
        "package": string,
        "messages": Array<any>,
        "enums": Array<any>,
        "imports": Array<any>,
        "options": any,
    } {
        let topLevel: any = {
            "name": "[ROOT]", // temporary
            "package": null,
            "messages": [],
            "enums": [],
            "imports": [],
            "options": {},
        };
        let token, header = true;
        do {
            token = this.tn.next();
            if (token == null) {
                break; // No more messages
            }
            if (token == 'package') {
                if (!header) {
                    throw (new Error("Illegal package definition at line " + this.tn.line + ": Must be declared before the first message or enum"));
                }
                if (topLevel["package"] !== null) {
                    throw (new Error("Illegal package definition at line " + this.tn.line + ": Package already declared"));
                }
                topLevel["package"] = this._parsePackage(token);
            } else if (token == 'import') {
                if (!header) {
                    throw (new Error("Illegal import definition at line " + this.tn.line + ": Must be declared before the first message or enum"));
                }
                let temp: string = <string>token;
                topLevel.imports.push(this._parseImport(temp));
            } else if (token === 'message') {
                this._parseMessage(topLevel, token);
                header = false;
            } else if (token === 'enum') {
                this._parseEnum(topLevel, token);
                header = false;
            } else if (token === 'option') {
                if (!header) {
                    throw (new Error("Illegal option definition at line " + this.tn.line + ": Must be declared before the first message or enum"));
                }
                this._parseOption(topLevel, token);
            } else if (token === 'extend') {
                this._parseExtend(topLevel, token);
            } else if (token === 'syntax') {
                this._parseIgnoredStatement(topLevel, token);
            } else {
                throw (new Error("Illegal top level declaration at line " + this.tn.line + ": " + token));
            }
        } while (true);
        delete topLevel["name"];
        return topLevel;
    };

    /**
     * Parses a number value.
     * @param {string} val Number value to parse
     * @return {number} Number
     * @throws {Error} If the number value is invalid
     * @private
     */
    private _parseNumber(val: string): number {
        var sign = 1;
        if (val.charAt(0) == '-') {
            sign = -1; val = val.substring(1);
        }
        if (Lang.NUMBER_DEC.test(val)) {
            return sign * parseInt(val, 10);
        } else if (Lang.NUMBER_HEX.test(val)) {
            return sign * parseInt(val.substring(2), 16);
        } else if (Lang.NUMBER_OCT.test(val)) {
            return sign * parseInt(val.substring(1), 8);
        } else if (Lang.NUMBER_FLT.test(val)) {
            return sign * parseFloat(val);
        }
        throw (new Error("Illegal number value at line " + this.tn.line + ": " + (sign < 0 ? '-' : '') + val));
    };

    /**
     * Parses an ID value.
     * @param {string} val ID value to parse
     * @param {boolean=} neg Whether the ID may be negative, defaults to `false`
     * @returns {number} ID
     * @throws {Error} If the ID value is invalid
     * @private
     */
    public _parseId(val: string, neg: boolean) {
        var id = -1;
        var sign = 1;
        if (val.charAt(0) == '-') {
            sign = -1; val = val.substring(1);
        }
        if (Lang.NUMBER_DEC.test(val)) {
            id = parseInt(val);
        } else if (Lang.NUMBER_HEX.test(val)) {
            id = parseInt(val.substring(2), 16);
        } else if (Lang.NUMBER_OCT.test(val)) {
            id = parseInt(val.substring(1), 8);
        } else {
            throw (new Error("Illegal ID value at line " + this.tn.line + ": " + (sign < 0 ? '-' : '') + val));
        }
        id = (sign * id) | 0; // Force to 32bit
        if (!neg && id < 0) {
            throw (new Error("Illegal ID range at line " + this.tn.line + ": " + (sign < 0 ? '-' : '') + val));
        }
        return id;
    };

    /**
     * Parses the package definition.
     * @param {string} token Initial token
     * @return {string} Package name
     * @throws {Error} If the package definition cannot be parsed
     * @private
     */
    public _parsePackage(token: string) {
        token = <string>this.tn.next();
        if (!Lang.TYPEREF.test(token)) {
            throw (new Error("Illegal package name at line " + this.tn.line + ": " + token));
        }
        var pkg = token;
        token = <string>this.tn.next();
        if (token != Lang.END) {
            throw (new Error("Illegal end of package definition at line " + this.tn.line + ": " + token + " ('" + Lang.END + "' expected)"));
        }
        return pkg;
    };

    /**
     * Parses an import definition.
     * @param {string} token Initial token
     * @return {string} Import file name 
     * @throws {Error} If the import definition cannot be parsed
     * @private
     */
    public _parseImport(token: string) {
        token = <string>this.tn.next();
        if (token === "public") {
            token = <string>this.tn.next();
        }
        if (token !== Lang.STRINGOPEN && token !== Lang.STRINGOPEN_SQ) {
            throw (new Error("Illegal begin of import value at line " + this.tn.line + ": " + token + " ('" + Lang.STRINGOPEN + "' or '" + Lang.STRINGOPEN_SQ + "' expected)"));
        }
        var imported = this.tn.next();
        token = <string>this.tn.next();
        if (token !== this.tn.stringEndsWith) {
            throw (new Error("Illegal end of import value at line " + this.tn.line + ": " + token + " ('" + this.tn.stringEndsWith + "' expected)"));
        }
        token = <string>this.tn.next();
        if (token !== Lang.END) {
            throw (new Error("Illegal end of import definition at line " + this.tn.line + ": " + token + " ('" + Lang.END + "' expected)"));
        }
        return imported;
    };

    /**
     * Parses a namespace option.
     * @param {Object} parent Parent definition
     * @param {string} token Initial token
     * @throws {Error} If the option cannot be parsed
     * @private
     */
    public _parseOption(parent: any, token: string) {
        token = <string>this.tn.next();
        var custom = false;
        if (token == Lang.COPTOPEN) {
            custom = true;
            token = <string>this.tn.next();
        }
        if (!Lang.NAME.test(token)) {
            // we can allow options of the form google.protobuf.* since they will just get ignored anyways
            if (!/google\.protobuf\./.test(token)) {
                throw (new Error("Illegal option name in message " + parent.name + " at line " + this.tn.line + ": " + token));
            }
        }
        var name = token;
        token = <string>this.tn.next();
        if (custom) { // (my_method_option).foo, (my_method_option), some_method_option
            if (token !== Lang.COPTCLOSE) {
                throw (new Error("Illegal custom option name delimiter in message " + parent.name + ", option " + name + " at line " + this.tn.line + ": " + token + " ('" + Lang.COPTCLOSE + "' expected)"));
            }
            name = '(' + name + ')';
            token = <string>this.tn.next();
            if (Lang.FQTYPEREF.test(token)) {
                name += token;
                token = <string>this.tn.next();
            }
        }
        if (token !== Lang.EQUAL) {
            throw (new Error("Illegal option operator in message " + parent.name + ", option " + name + " at line " + this.tn.line + ": " + token + " ('" + Lang.EQUAL + "' expected)"));
        }
        var value;
        token = <string>this.tn.next();
        if (token === Lang.STRINGOPEN || token === Lang.STRINGOPEN_SQ) {
            value = this.tn.next();
            token = <string>this.tn.next();
            if (token !== this.tn.stringEndsWith) {
                throw (new Error("Illegal end of option value in message " + parent.name + ", option " + name + " at line " + this.tn.line + ": " + token + " ('" + this.tn.stringEndsWith + "' expected)"));
            }
        } else {
            if (Lang.NUMBER.test(token)) {
                value = this._parseNumber(token);
            } else if (Lang.TYPEREF.test(token)) {
                value = token;
            } else {
                throw (new Error("Illegal option value in message " + parent.name + ", option " + name + " at line " + this.tn.line + ": " + token));
            }
        }
        token = <string>this.tn.next();
        if (token !== Lang.END) {
            throw (new Error("Illegal end of option in message " + parent.name + ", option " + name + " at line " + this.tn.line + ": " + token + " ('" + Lang.END + "' expected)"));
        }
        parent["options"][name] = value;
    };

    /**
     * Parses an ignored block of the form ['keyword', 'typeref', '{' ... '}'].
     * @param {Object} parent Parent definition
     * @param {string} keyword Initial token
     * @throws {Error} If the directive cannot be parsed
     * @private
     */
    public _parseIgnoredBlock(parent: any, keyword: string) {
        var token = <string>this.tn.next();
        if (!Lang.TYPEREF.test(token)) {
            throw (new Error("Illegal " + keyword + " type in " + parent.name + ": " + token));
        }
        var name = token;
        token = <string>this.tn.next();
        if (token !== Lang.OPEN) {
            throw (new Error("Illegal OPEN in " + parent.name + " after " + keyword + " " + name + " at line " + this.tn.line + ": " + token));
        }
        var depth = 1;
        do {
            token = <string>this.tn.next();
            if (token === null) {
                throw (new Error("Unexpected EOF in " + parent.name + ", " + keyword + " (ignored) at line " + this.tn.line + ": " + name));
            }
            if (token === Lang.OPEN) {
                depth++;
            } else if (token === Lang.CLOSE) {
                token = <string>this.tn.peek();
                if (token === Lang.END) this.tn.next();
                depth--;
                if (depth === 0) {
                    break;
                }
            }
        } while (true);
    };

    /**
     * Parses an ignored statement of the form ['keyword', ..., ';'].
     * @param {Object} parent Parent definition
     * @param {string} keyword Initial token
     * @throws {Error} If the directive cannot be parsed
     * @private
     */
    public _parseIgnoredStatement(parent: any, keyword: string) {
        var token;
        do {
            token = this.tn.next();
            if (token === null) {
                throw (new Error("Unexpected EOF in " + parent.name + ", " + keyword + " (ignored) at line " + this.tn.line));
            }
            if (token === Lang.END) break;
        } while (true);
    };



    /**
     * Parses a message definition.
     * @param {Object} parent Parent definition
     * @param {string} token First token
     * @return {Object}
     * @throws {Error} If the message cannot be parsed
     * @private
     */
    public _parseMessage(parent: any, token: string) {
        /** @dict */
        var msg: any = {}; // Note: At some point we might want to exclude the parser, so we need a dict.
        token = <string>this.tn.next();
        if (!Lang.NAME.test(token)) {
            throw (new Error("Illegal message name" + (parent ? " in message " + parent["name"] : "") + " at line " + this.tn.line + ": " + token));
        }
        msg["name"] = token;
        token = <string>this.tn.next();
        if (token != Lang.OPEN) {
            throw (new Error("Illegal OPEN after message " + msg.name + " at line " + this.tn.line + ": " + token + " ('" + Lang.OPEN + "' expected)"));
        }
        msg["fields"] = []; // Note: Using arrays to support also browser that cannot preserve order of object keys.
        msg["enums"] = [];
        msg["messages"] = [];
        msg["options"] = {};
        // msg["extensions"] = undefined
        do {
            token = <string>this.tn.next();
            if (token === Lang.CLOSE) {
                token = <string>this.tn.peek();
                if (token === Lang.END) this.tn.next();
                break;
            } else if (Lang.RULE.test(token)) {
                this._parseMessageField(msg, token);
            } else if (token === "enum") {
                this._parseEnum(msg, token);
            } else if (token === "message") {
                this._parseMessage(msg, token);
            } else if (token === "option") {
                this._parseOption(msg, token);
            } else if (token === "extensions") {
                msg["extensions"] = this._parseExtensions(msg, token);
            } else if (token === "extend") {
                this._parseExtend(msg, token);
            } else {
                throw (new Error("Illegal token in message " + msg.name + " at line " + this.tn.line + ": " + token + " (type or '" + Lang.CLOSE + "' expected)"));
            }
        } while (true);
        parent["messages"].push(msg);
        return msg;
    };

    /**
     * Parses a message field.
     * @param {Object} msg Message definition
     * @param {string} token Initial token
     * @throws {Error} If the message field cannot be parsed
     * @private
     */
    public _parseMessageField(msg: any, token: string) {
        /** @dict */
        var fld: any = {};
        fld["rule"] = token;
        token = <string>this.tn.next();
        if (!Lang.TYPE.test(token) && !Lang.TYPEREF.test(token)) {
            throw (new Error("Illegal field type in message " + msg.name + " at line " + this.tn.line + ": " + token));
        }
        fld["type"] = token;
        token = <string>this.tn.next();
        if (!Lang.NAME.test(token)) {
            throw (new Error("Illegal field name in message " + msg.name + " at line " + this.tn.line + ": " + token));
        }
        fld["name"] = token;
        token = <string>this.tn.next();
        if (token !== Lang.EQUAL) {
            throw (new Error("Illegal field number operator in message " + msg.name + "#" + fld.name + " at line " + this.tn.line + ": " + token + " ('" + Lang.EQUAL + "' expected)"));
        }
        token = <string>this.tn.next();
        try {
            fld["id"] = this._parseId(token, false);
        } catch (e) {
            throw (new Error("Illegal field id in message " + msg.name + "#" + fld.name + " at line " + this.tn.line + ": " + token));
        }
        /** @dict */
        fld["options"] = {};
        token = <string>this.tn.next();
        if (token === Lang.OPTOPEN) {
            this._parseFieldOptions(msg, fld, token);
            token = <string>this.tn.next();
        }
        if (token !== Lang.END) {
            throw (new Error("Illegal field delimiter in message " + msg.name + "#" + fld.name + " at line " + this.tn.line + ": " + token + " ('" + Lang.END + "' expected)"));
        }
        msg["fields"].push(fld);
    };

    /**
     * Parses a set of field option definitions.
     * @param {Object} msg Message definition
     * @param {Object} fld Field definition
     * @param {string} token Initial token
     * @throws {Error} If the message field options cannot be parsed
     * @private
     */
    private _parseFieldOptions(msg: any, fld: any, token: string) {
        var first = true;
        do {
            token = <string>this.tn.next();
            if (token === Lang.OPTCLOSE) {
                break;
            } else if (token === Lang.OPTEND) {
                if (first) {
                    throw (new Error("Illegal start of message field options in message " + msg.name + "#" + fld.name + " at line " + this.tn.line + ": " + token));
                }
                token = <string>this.tn.next();
            }
            this._parseFieldOption(msg, fld, token);
            first = false;
        } while (true);
    };

    /**
     * Parses a single field option.
     * @param {Object} msg Message definition
     * @param {Object} fld Field definition
     * @param {string} token Initial token
     * @throws {Error} If the mesage field option cannot be parsed
     * @private
     */
    public _parseFieldOption(msg: any, fld: any, token: string) {
        var custom = false;
        if (token === Lang.COPTOPEN) {
            token = <string>this.tn.next();
            custom = true;
        }
        if (!Lang.NAME.test(token)) {
            throw (new Error("Illegal field option in message " + msg.name + "#" + fld.name + " at line " + this.tn.line + ": " + token));
        }
        var name = token;
        token = <string>this.tn.next();
        if (custom) {
            if (token !== Lang.COPTCLOSE) {
                throw (new Error("Illegal custom field option name delimiter in message " + msg.name + "#" + fld.name + " at line " + this.tn.line + ": " + token + " (')' expected)"));
            }
            name = '(' + name + ')';
            token = <string>this.tn.next();
            if (Lang.FQTYPEREF.test(token)) {
                name += token;
                token = <string>this.tn.next();
            }
        }
        if (token !== Lang.EQUAL) {
            throw (new Error("Illegal field option operation in message " + msg.name + "#" + fld.name + " at line " + this.tn.line + ": " + token + " ('=' expected)"));
        }
        var value;
        token = <string>this.tn.next();
        if (token === Lang.STRINGOPEN || token === Lang.STRINGOPEN_SQ) {
            value = this.tn.next();
            token = <string>this.tn.next();
            if (token != this.tn.stringEndsWith) {
                throw (new Error("Illegal end of field value in message " + msg.name + "#" + fld.name + ", option " + name + " at line " + this.tn.line + ": " + token + " ('" + this.tn.stringEndsWith + "' expected)"));
            }
        } else if (Lang.NUMBER.test(token)) {
            value = this._parseNumber(token);
        } else if (Lang.BOOL.test(token)) {
            value = token.toLowerCase() === 'true';
        } else if (Lang.TYPEREF.test(token)) {
            value = token; // TODO: Resolve?
        } else {
            throw (new Error("Illegal field option value in message " + msg.name + "#" + fld.name + ", option " + name + " at line " + this.tn.line + ": " + token));
        }
        fld["options"][name] = value;
    };

    /**
     * Parses an enum.
     * @param {Object} msg Message definition
     * @param {string} token Initial token
     * @throws {Error} If the enum cannot be parsed
     * @private
     */
    public _parseEnum(msg: any, token: string) {
        /** @dict */
        var enm: any = {};
        token = <string>this.tn.next();
        if (!Lang.NAME.test(token)) {
            throw (new Error("Illegal enum name in message " + msg.name + " at line " + this.tn.line + ": " + token));
        }
        enm["name"] = token;
        token = <string>this.tn.next();
        if (token !== Lang.OPEN) {
            throw (new Error("Illegal OPEN after enum " + enm.name + " at line " + this.tn.line + ": " + token));
        }
        enm["values"] = [];
        enm["options"] = {};
        do {
            token = <string>this.tn.next();
            if (token === Lang.CLOSE) {
                token = <string>this.tn.peek();
                if (token === Lang.END) this.tn.next();
                break;
            }
            if (token == 'option') {
                this._parseOption(enm, token);
            } else {
                if (!Lang.NAME.test(token)) {
                    throw (new Error("Illegal enum value name in enum " + enm.name + " at line " + this.tn.line + ": " + token));
                }
                this._parseEnumValue(enm, token);
            }
        } while (true);
        msg["enums"].push(enm);
    };

    /**
     * Parses an enum value.
     * @param {Object} enm Enum definition
     * @param {string} token Initial token
     * @throws {Error} If the enum value cannot be parsed
     * @private
     */
    public _parseEnumValue(enm: any, token: string) {
        /** @dict */
        var val: any = {};
        val["name"] = token;
        token = <string>this.tn.next();
        if (token !== Lang.EQUAL) {
            throw (new Error("Illegal enum value operator in enum " + enm.name + " at line " + this.tn.line + ": " + token + " ('" + Lang.EQUAL + "' expected)"));
        }
        token = <string>this.tn.next();
        try {
            val["id"] = this._parseId(token, true);
        } catch (e) {
            throw (new Error("Illegal enum value id in enum " + enm.name + " at line " + this.tn.line + ": " + token));
        }
        enm["values"].push(val);
        token = <string>this.tn.next();
        if (token === Lang.OPTOPEN) {
            var opt = { 'options': {} }; // TODO: Actually expose them somehow.
            this._parseFieldOptions(enm, opt, token);
            token = <string>this.tn.next();
        }
        if (token !== Lang.END) {
            throw (new Error("Illegal enum value delimiter in enum " + enm.name + " at line " + this.tn.line + ": " + token + " ('" + Lang.END + "' expected)"));
        }
    };

    /**
     * Parses an extensions statement.
     * @param {Object} msg Message object
     * @param {string} token Initial token
     * @throws {Error} If the extensions statement cannot be parsed
     * @private
     */
    public _parseExtensions(msg: any, token: string) {
        /** @type {Array.<number>} */
        var range = [];
        token = <string>this.tn.next();
        if (token === "min") { // FIXME: Does the official implementation support this?
            range.push(Lang.ID_MIN);
        } else if (token === "max") {
            range.push(Lang.ID_MAX);
        } else {
            range.push(this._parseNumber(token));
        }
        token = <string>this.tn.next();
        if (token !== 'to') {
            throw ("Illegal extensions delimiter in message " + msg.name + " at line " + this.tn.line + " ('to' expected)");
        }
        token = <string>this.tn.next();
        if (token === "min") {
            range.push(Lang.ID_MIN);
        } else if (token === "max") {
            range.push(Lang.ID_MAX);
        } else {
            range.push(this._parseNumber(token));
        }
        token = <string>this.tn.next();
        if (token !== Lang.END) {
            throw (new Error("Illegal extension delimiter in message " + msg.name + " at line " + this.tn.line + ": " + token + " ('" + Lang.END + "' expected)"));
        }
        return range;
    };

    /**
     * Parses an extend block.
     * @param {Object} parent Parent object
     * @param {string} token Initial token
     * @throws {Error} If the extend block cannot be parsed
     * @private
     */
    public _parseExtend(parent: any, token: string) {
        token = <string>this.tn.next();
        if (!Lang.TYPEREF.test(token)) {
            throw (new Error("Illegal extended message name at line " + this.tn.line + ": " + token));
        }
        /** @dict */
        var ext: any = {};
        ext["ref"] = token;
        ext["fields"] = [];
        token = <string>this.tn.next();
        if (token !== Lang.OPEN) {
            throw (new Error("Illegal OPEN in extend " + ext.name + " at line " + this.tn.line + ": " + token + " ('" + Lang.OPEN + "' expected)"));
        }
        do {
            token = <string>this.tn.next();
            if (token === Lang.CLOSE) {
                token = <string>this.tn.peek();
                if (token == Lang.END) this.tn.next();
                break;
            } else if (Lang.RULE.test(token)) {
                this._parseMessageField(ext, token);
            } else {
                throw (new Error("Illegal token in extend " + ext.name + " at line " + this.tn.line + ": " + token + " (rule or '" + Lang.CLOSE + "' expected)"));
            }
        } while (true);
        parent["messages"].push(ext);
        return ext;
    };

    /**
     * Returns a string representation of this object.
     * @returns {string} String representation as of "Parser"
     */
    public toString() {
        return "Parser";
    };
}